package coverage

import (
	"bufio"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
)

// Run executes `go test` with coverage and writes the profile to profilePath.
// If pkgs is empty, it runs `./...`.
func Run(profilePath string, pkgs ...string) error {
	if profilePath == "" {
		profilePath = "coverage.out"
	}
	args := []string{"test", "-coverprofile=" + profilePath}
	if len(pkgs) == 0 {
		args = append(args, "./...")
	} else {
		args = append(args, pkgs...)
	}
	cmd := exec.Command("go", args...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

// Analyze compares the coverage profile with runtime dependencies and
// returns slices of covered and uncovered dependency paths. Only dependencies
// under repoRoot are considered.
func Analyze(profilePath, runtimeDepsPath, modulePath, repoRoot string) ([]string, []string, error) {
	if modulePath == "" {
		return nil, nil, errors.New("module path is empty")
	}
	prof, err := os.Open(profilePath)
	if err != nil {
		return nil, nil, err
	}
	defer prof.Close()

	covered := make(map[string]bool)
	scanner := bufio.NewScanner(prof)
	first := true
	for scanner.Scan() {
		line := scanner.Text()
		if first {
			first = false
			continue
		}
		i := strings.Index(line, ":")
		if i == -1 {
			continue
		}
		p := line[:i]
		rel := strings.TrimPrefix(p, modulePath+"/")
		abs := filepath.Join(repoRoot, rel)
		abs = filepath.Clean(abs)

		fields := strings.Fields(line[i+1:])
		if len(fields) < 2 {
			continue
		}
		count, err := strconv.Atoi(fields[len(fields)-1])
		if err != nil {
			continue
		}
		if count > 0 {
			covered[abs] = true
		} else if _, ok := covered[abs]; !ok {
			covered[abs] = false
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, nil, err
	}

	data, err := os.ReadFile(runtimeDepsPath)
	if err != nil {
		return nil, nil, err
	}
	var deps []string
	if err := json.Unmarshal(data, &deps); err != nil {
		return nil, nil, err
	}

	var coveredDeps, uncoveredDeps []string
	for _, d := range deps {
		if !strings.HasPrefix(d, repoRoot) {
			continue
		}
		d = filepath.Clean(d)
		if covered[d] {
			coveredDeps = append(coveredDeps, d)
		} else {
			uncoveredDeps = append(uncoveredDeps, d)
		}
	}

	return coveredDeps, uncoveredDeps, nil
}

// Report generates an HTML coverage report with dependency coverage
// statistics inserted. It also prints uncovered dependencies to stdout.
func Report(profilePath, runtimeDepsPath, htmlPath, modulePath, repoRoot string) error {
	if htmlPath == "" {
		htmlPath = "coverage_report.html"
	}
	cmd := exec.Command("go", "tool", "cover", "-html="+profilePath, "-o", htmlPath)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return err
	}

	coveredDeps, uncoveredDeps, err := Analyze(profilePath, runtimeDepsPath, modulePath, repoRoot)
	if err != nil {
		return err
	}

	total := len(coveredDeps) + len(uncoveredDeps)
	percent := 0.0
	if total > 0 {
		percent = float64(len(coveredDeps)) / float64(total) * 100
	}

	summary := fmt.Sprintf("<h1>Dependency Coverage</h1><p>Covered %d/%d (%.1f%%)</p>", len(coveredDeps), total, percent)
	if len(uncoveredDeps) > 0 {
		summary += "<p>Uncovered dependencies:</p><ul>"
		for _, u := range uncoveredDeps {
			summary += "<li>" + u + "</li>"
			fmt.Printf("uncovered dependency: %s\n", u)
		}
		summary += "</ul>"
	}

	data, err := os.ReadFile(htmlPath)
	if err != nil {
		return err
	}
	content := string(data)
	if idx := strings.Index(content, "<body>"); idx != -1 {
		idx += len("<body>")
		content = content[:idx] + summary + content[idx:]
	} else {
		content = summary + content
	}
	return os.WriteFile(htmlPath, []byte(content), 0644)
}

// modulePathFromGoMod reads the module path from go.mod in repoRoot.
func modulePathFromGoMod(repoRoot string) (string, error) {
	data, err := os.ReadFile(filepath.Join(repoRoot, "go.mod"))
	if err != nil {
		return "", err
	}
	for _, line := range strings.Split(string(data), "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "module ") {
			return strings.TrimSpace(strings.TrimPrefix(line, "module ")), nil
		}
	}
	return "", errors.New("module path not found in go.mod")
}

// Generate runs tests with coverage, analyzes runtime dependencies and
// produces coverage_report.html in the current working directory.
func Generate(runtimeDepsPath string) error {
	repoRoot, err := os.Getwd()
	if err != nil {
		return err
	}
	mod, err := modulePathFromGoMod(repoRoot)
	if err != nil {
		return err
	}
	profile := "coverage.out"
	if err := Run(profile); err != nil {
		return err
	}
	return Report(profile, runtimeDepsPath, "coverage_report.html", mod, repoRoot)
}
